/**
 * OPMERKINGEN OVER KLASSE:
 * 		checken of collisionObject isValidObject()? --> gescreven, staat vanonder, maar als het een collisionobject is gt da sowieso valid zijn 
 * 															of null nie?
 * 		bij getShips is er gecast --> gaat volgens mij moeilijk anders, of met instanceof erges iets forceren ma da gt nie beter zijn 
 * 		bij evolve omwille van recursiviteit evt geheugenproblemen aanpakken (bij grote dt)
 * 		nog na te kijken: 	Of destroy goed gaat
 * 							Of bij het setten van world in ship het hier ook wordt toegevoegd ( addAsC..O...()  )
 */

package asteroids.model;

import java.util.*;

import be.kuleuven.cs.som.annotate.*;

import asteroids.CollisionListener;
import asteroids.ModelException;
import asteroids.Util;

public class World {


	/**
	 * 
	 * @param height
	 * @param width
	 * @Invar |isValidSize()
	 */
	public World(double height, double width){
		if(!isValidWorldSize(height, width))
			throw new OutOfBoundsException("The given width and height are not applicable to create a world");
		this.width = width;
		this.height = height;

		gameObjects = new HashSet<CollisionObject>();
	}


	private final double maxHeight 	= 	Double.MAX_VALUE;
	private final double maxWidth 	=	Double.MAX_VALUE;
	private final double minHeight	=	0;
	private final double minWidth	=	0;
	private final double height;
	private final double width;
	private final Set<CollisionObject> gameObjects;




	/**
	 * 
	 * @param height
	 * @param width
	 * @return 	...
	 * 			|return (getHeight*getWidth)<getMaxWorldSize() && !double.isNan(getHeight()*getWidth()) && double.i
	 */
	private boolean isValidWorldSize(double height, double width){
		return (isValidHeight(height) && isValidWidth(width));
	}


	/**
	 * Get the height of the world
	 * @return The height of the world
	 */
	@Basic @Immutable
	public double getWorldHeight(){
		return this.height;
	}

	@Basic @Immutable
	private double getMinHeight(){
		return this.minHeight;
	}

	@Basic @Immutable
	private double getMaxHeight(){
		return this.maxHeight;
	}

	private boolean isValidHeight(double height){
		if(!isLocatedWithinRange(getMinHeight(), getMaxHeight(), height) ||Double.isNaN(height)){
			return false;
		}
		return true;
	}	



	@Basic	@Immutable
	public double getWorldWidth(){
		return this.width;
	}

	@Basic @Immutable
	private double getMinWidth(){
		return this.minWidth;
	}

	@Basic @Immutable
	private double getMaxWidth(){
		return this.maxWidth;
	}

	private boolean isValidWidth(double width){

		if(!isLocatedWithinRange(getMinWidth(), getMaxWidth(), width) ||Double.isNaN(width)){
			return false;
		}
		return true;
	}
	
	
	
	//TODO checken, wordt gebruikt in collisionobject in exceedsworldboundarys
	public double getLeftBoundary(){
		return 0;
	}
	
	public double getRightBoundary(){
		return getWorldWidth();
	}
	
	public double getLowerBoundary(){
		return 0;
	}
	
	public double getUpperBoundary(){
		return getWorldHeight();
	}
	
	private boolean isValidObject(CollisionObject collisionObject){
		if(collisionObject.isShip()){
			return true;
		}
		else if(collisionObject.isAsteroid()){
			return true;
		}
		else if(collisionObject.isBullet()){
			return true;
		}
		else return false;
	}
	
	
	
	
	
	
	
	
	
	
	
	
	
	




	private Set<CollisionObject> getGameObjects(){
		Set<CollisionObject> returnObjectSet = new HashSet<CollisionObject>();
		Iterator<CollisionObject> it = gameObjects.iterator();
		while(it.hasNext()){
			CollisionObject colissionObject = it.next();
			returnObjectSet.add(colissionObject);
		}
		return returnObjectSet;
	}

	/**
	 * Returns the size of this vector
	 * @return The size of this vector
	 */
	private int getAmountOfGameObjects(){
		return this.getGameObjects().size();
	}

	public boolean hasAsCollisionObject(CollisionObject collisionObject){
		return getGameObjects().contains(collisionObject);
	}

	public void addAsCollisionObject(CollisionObject collisionObject){
		if(canHaveAsCollisionObject(collisionObject) && collisionObject.isInNoWorld()){
			this.gameObjects.add(collisionObject);
			if(collisionObject.getWorld() != this)
			collisionObject.setWorld(this);
		}

	}

	//binding via wereld ook verwijderen
	// TODO modelexception?
	/**
	 * DIT NIET ALS DESTROYOBJECT OPNEMEN, SHIP ZOU EVENTUEEL LATER OOK TUSSEN WERELDEN KUNNEN REIZEN?
	 * @param collisionObject
	 * @throws ModelException
	 */
	public void removeAsCollisionObject(CollisionObject collisionObject) throws ModelException{
		if(hasAsCollisionObject(collisionObject)){
			this.gameObjects.remove(collisionObject);
			collisionObject.setWorld(null);
		}
		else
			throw new ModelException("This object does not exists");
	}

	public void destroyCollisionObject(CollisionObject collisionObject){
		removeAsCollisionObject(collisionObject);
		collisionObject.destroy();
	}
// TODO tweede object meegeven om te checken of het dezelde world is?
	public boolean canHaveAsCollisionObject(CollisionObject collisionObject){
		if (collisionObject!=null && !collisionObject.isDestroyed() )
			//if(collisionObject.isInNoWorld())
			//if (this.hasAsCollisionObject(collisionObject));
			return true;
		return false;
	}

	/**
	 * 
	 * @return	True if all objects are valid or when there are none in the world
	 */
	public boolean hasProperCollisionObjects(){

		Iterator<CollisionObject> it = gameObjects.iterator();
		while(it.hasNext()){
			CollisionObject collisionObject = it.next();
			if(collisionObject.getWorld() != this && !this.canHaveAsCollisionObject(collisionObject) && collisionObject.canHaveAsWorld(this))
				return false;
		}
		return false;
	}

	/**
	 * 
	 * @param dimension
	 * @return	...
	 * 			| return (dimension>=getLowerBound() && dimension<= getUpperBound() && !Double.isNan(dimension) && dimension!=null;
	 */
	private static boolean isLocatedWithinRange(double minimum,double maximum, double value){
		if(Util.fuzzyLessThanOrEqualTo(value, maximum)){			//Smaller than maximum
			if(Util.fuzzyEquals(value, minimum))					//equal to minimumvalue
				return true;
			else if(!Util.fuzzyLessThanOrEqualTo(value, minimum))	//Greater than minimum
				return true;
			return false;
		}
		return false;
	}




	/**
	 * Return all ships located within <code>world</code>.
	 */
	public Set<Ship> getShips(){
		Set<Ship> resultSet = new HashSet<Ship>();
		Iterator<CollisionObject> it = gameObjects.iterator();
		while(it.hasNext()){
			CollisionObject collisionObject = it.next();
			try{
			if(collisionObject.isShip());
			resultSet.add((Ship) collisionObject);}
			catch (ClassCastException exc){
				
			}
		}
		return resultSet;

	}

	/**
	 * Return all asteroids located in <code>world</code>.
	 */
	public Set<Asteroid> getAsteroids(){
		Set<Asteroid> resultSet = new HashSet<Asteroid>();
		Iterator<CollisionObject> it = gameObjects.iterator();
		while(it.hasNext()){
			CollisionObject collisionObject = it.next();
			try{if(collisionObject.isAsteroid());
			resultSet.add((Asteroid)collisionObject);}
			catch(ClassCastException exc){
				
			}
		}
		return resultSet;
	}

	/**
	 * Return all bullets located in <code>world</code>.
	 */
	public Set<Bullet> getBullets(){
		Set<Bullet> resultSet = new HashSet<Bullet>();
		Iterator<CollisionObject> it = gameObjects.iterator();
		while(it.hasNext()){
			CollisionObject collisionObject = it.next();
			try{if(collisionObject.isBullet());
			resultSet.add((Bullet)collisionObject);}
			catch(ClassCastException exc){
				
			}
		}
		return resultSet;
	}




	/**
	 * Add <code>ship</code> to <code>world</code>.
	 */
	public void addShip(World world, Ship ship){
		//TODO
		//Naar facade met createShip en dan setWorld ofzoiets 
		//Met try catch doen, catch wnr hij niet in wereld kan, ship dan vernietigen.
	}
	
	/**
	 * Add <code>asteroid</code> to <code>world</code>.
	 */
	public void addAsteroid(World world, Asteroid asteroid){
		//TODO
		//Naar facade met createShip en dan setWorld ofzoiets 
		//Met try catch doen, catch wnr hij niet in wereld kan, ship dan vernietigen.
	}

	/**
	 * Remove <code>ship</code> from <code>world</code>.
	 */
	public void removeShip(World world, Ship ship){
		//TODO
		//Naar facade met removeAsCollisionObject (eerst checken of het wel een ship is
	}

	/**
	 * Remove <code>asteroid</code> from <code>world</code>.
	 */
	public void removeAsteroid(World world, Asteroid asteroid){
		//TODO
	}





	/**
	 * Advance <code>world</code> by <code>dt<code> seconds. 
	 * 
	 * To enable explosions within the UI, notify <code>collisionListener</code>
	 * whenever an entity collides with a boundary or another entity during this
	 * method. <code>collisionListener</code> may be null. If
	 * <code>collisionListener</code> is <code>null</code>, do not call its notify
	 * methods.
	 */
	public void evolve2(double dt){
		//EIGEN KEUZE VOOR DT ONEINDIG OF 0 OF NEGATIEF OF ISNAN
		if(Double.isNaN(dt) || dt == Double.POSITIVE_INFINITY || dt== Double.NEGATIVE_INFINITY)
			throw new IllegalTimeDurationException("Invalid timeduration dt");
		
		if(!Util.fuzzyLessThanOrEqualTo(dt, 0)){			//else do nothing
			//STEP1: FIRST COLLISION
			double timeToFirstCollision = Double.POSITIVE_INFINITY;
			CollisionObject firstCollisionObject 		= null;
			CollisionObject firstCollisionObjectPartner	= null;
			Iterator<CollisionObject> itFirstObject		 = gameObjects.iterator();
			Iterator<CollisionObject> itCollisionPartner = gameObjects.iterator();
			while(itFirstObject.hasNext()){
				CollisionObject colissionObject = itFirstObject.next();
				while(itCollisionPartner.hasNext()){
					CollisionObject colissionObjectPartner = itCollisionPartner.next();
					if(colissionObject != null && colissionObject != colissionObjectPartner){	//impliciet getest of colisisionObjectPartner ook niet null is
						double collisionTime = colissionObject.getTimeToCollision(colissionObjectPartner);
						if(collisionTime < timeToFirstCollision){
							timeToFirstCollision = collisionTime;
							firstCollisionObject = colissionObject;
							firstCollisionObjectPartner = colissionObjectPartner;
						}
					}
				}
			}

			//STEP2: ADVANCE ALL SHIPS BY TIMETOFIRSTCOLLISION OR GO TO STEP 5
			if(timeToFirstCollision < dt){  // TODO fuzzyequals
				for(CollisionObject collisionObject: getGameObjects()){
					collisionObject.move(timeToFirstCollision);
				}

				//STEP3: RESOLVE COLLISION
				//TODO	IK DENK ZO:
				firstCollisionObject.collisionReactionWith(firstCollisionObjectPartner);

				//STEP4: SUBSTRACT TIMETOFIRSTCOLLSION AND GO TO STEP1
				//TODO NIET ZEKER OF JE ER ZO UITKOMT, WEL ELEGANT...
				evolve(dt-timeToFirstCollision);
			}
			else{
				for(CollisionObject collisionObject: getGameObjects()){
					collisionObject.move(dt);
					if(collisionObject.isShip()){
						if( ((Ship) collisionObject).isShipThrusterActive()){
							double newXVelocity =	(((Ship) collisionObject).getXAcceleration()) *dt +	collisionObject.getXVelocity();
							double newYVelocity = 	(((Ship) collisionObject).getYAcceleration()) *dt +	collisionObject.getYVelocity();
							if(collisionObject.isValidVelocity(newXVelocity, newYVelocity)){
								collisionObject.setXVelocity(newXVelocity);
								collisionObject.setYVelocity(newYVelocity);
								break;
							}
							else{
								double vectorAngle = CollisionObject.getResAngleOfPerpendicularVectors(newXVelocity, newYVelocity);
								collisionObject.setXVelocity(collisionObject.getSpeedOfLight()*Math.cos(vectorAngle));
								collisionObject.setYVelocity(collisionObject.getSpeedOfLight()*Math.sin(vectorAngle));
								break;
							}
						}
					}
				}
				//NIET ZEKER OF DIT MOET MAAR IK DOE HET OM "ZEKER" UIT LUS TE GERAKEN
				dt = 0;
			}
		}
		
	}


	
	
	
	
	
	
	public void evolve(double dt){
		//PREDICT FIRST COLLISION
		for(CollisionObject object1: this.getGameObjects()){
			for(CollisionObject object2: this.getGameObjects()){
				if(object1 == object2){
					break;
				}
				
			}
		}
	}
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	








}